/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.stetho.sample;

import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.OperationApplicationException;
import android.net.Uri;
import android.os.RemoteException;
import android.util.Log;
import android.util.Xml;
import com.facebook.stetho.common.Utf8Charset;
import com.facebook.stetho.common.Util;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import javax.annotation.Nullable;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

public class APODRssFetcher {
  private static final String TAG = "APODRssFetcher";
  
  private static final String APOD_RSS_URL = "https://apod.nasa.gov/apod.rss";

  private final ContentResolver mContentResolver;

  public APODRssFetcher(ContentResolver contentResolver) {
    mContentResolver = contentResolver;
  }

  public void fetchAndStore() {
    Networker.HttpRequest request = Networker.HttpRequest.newBuilder()
        .friendlyName("APOD RSS")
        .method(Networker.HttpMethod.GET)
        .url(APOD_RSS_URL)
        .build();
    Networker.get().submit(request, mStoreRssResponse);
  }

  private final Networker.Callback mStoreRssResponse = new Networker.Callback() {
    @Override
    public void onResponse(Networker.HttpResponse result) {
      if (result.statusCode == 200) {
        try {
          List<RssItem> rssItems = parseRss(result.body);
          List<ApodItem> apodItems = decorateRssItemsWithLinkImages(rssItems);
          store(apodItems);
        } catch (XmlPullParserException e) {
          Log.e(TAG, "Parse error", e);
        } catch (OperationApplicationException e) {
          Log.e(TAG, "Database write error", e);
        } catch (RemoteException e) {
          // Not recoverable, our process or the system_server must be dying...
          throw new RuntimeException(e);
        } catch (IOException e) {
          // Reading from a byte[] shouldn't cause this...
          throw new RuntimeException(e);
        }
      }
    }

    @Override
    public void onFailure(IOException e) {
      // Show in Stetho :)
    }

    private List<RssItem> parseRss(byte[] body) throws IOException, XmlPullParserException {
      XmlPullParser parser = Xml.newPullParser();
      parser.setInput(new ByteArrayInputStream(body), "UTF-8");
      List<RssItem> items = new RssParser(parser).parse();
      Log.d(TAG, "Fetched " + items.size() + " items");

      return items;
    }

    public List<ApodItem> decorateRssItemsWithLinkImages(List<RssItem> rssItems) {
      ArrayList<ApodItem> apodItems = new ArrayList<>(rssItems.size());
      final CountDownLatch fetchLinkLatch = new CountDownLatch(rssItems.size());
      for (RssItem rssItem : rssItems) {
        final ApodItem apodItem = new ApodItem();
        apodItem.rssItem = rssItem;
        fetchLinkPage(rssItem.link, new PageScrapedCallback() {
          @Override
          public void onPageScraped(@Nullable List<String> imageUrls) {
            apodItem.largeImageUrl = imageUrls != null && !imageUrls.isEmpty()
                ? imageUrls.get(0)
                : null;
            fetchLinkLatch.countDown();
          }
        });
        apodItems.add(apodItem);
      }

      // Wait for all link fetches to complete, despite running them in parallel...
      Util.awaitUninterruptibly(fetchLinkLatch);

      return apodItems;
    }

    private void store(List<ApodItem> items) throws RemoteException, OperationApplicationException {
      ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
      operations.add(
          ContentProviderOperation.newDelete(APODContract.CONTENT_URI)
              .build());
      for (ApodItem item : items) {
        Log.d(TAG, "Add item: " + item.rssItem.title);
        operations.add(
            ContentProviderOperation.newInsert(APODContract.CONTENT_URI)
                .withValues(convertItemToValues(item))
                .build());
      }

      mContentResolver.applyBatch(APODContract.AUTHORITY, operations);
    }

    private ContentValues convertItemToValues(ApodItem item) {
      ContentValues values = new ContentValues();
      values.put(APODContract.Columns.TITLE, item.rssItem.title);

      ArrayList<String> imageUrls = new ArrayList<>();
      String strippedText = HtmlScraper.parseWithImageTags(
          item.rssItem.description,
          null /* origin */,
          imageUrls);

      // Hack to remove some strange non-printing character at the start...
      strippedText = strippedText.substring(1).trim();
      String imageUrl = !imageUrls.isEmpty() ? imageUrls.get(0) : null;

      values.put(APODContract.Columns.DESCRIPTION_IMAGE_URL, imageUrl);
      values.put(APODContract.Columns.DESCRIPTION_TEXT, strippedText);
      values.put(APODContract.Columns.LARGE_IMAGE_URL, item.largeImageUrl);

      return values;
    }
  };

  private void fetchLinkPage(String linkUrl, PageScrapedCallback callback) {
    String originUrl = getOriginUri(Uri.parse(linkUrl)).toString();
    Networker.HttpRequest request = Networker.HttpRequest.newBuilder()
        .friendlyName("fetchLinkPage")
        .method(Networker.HttpMethod.GET)
        .url(linkUrl)
        .build();
    Networker.get().submit(request, new PageScrapeNetworkCallback(originUrl, callback));
  }

  private static Uri getOriginUri(Uri uri) {
    Uri.Builder b = uri.buildUpon();
    b.encodedPath(null);

    List<String> segments = uri.getPathSegments();
    for (int i = 0; i < segments.size() - 1; i++) {
      b.appendEncodedPath(segments.get(i));
    }

    return b.build();
  }

  private static class PageScrapeNetworkCallback implements Networker.Callback {
    @Nullable private final String mOrigin;
    private final PageScrapedCallback mDelegate;

    public PageScrapeNetworkCallback(@Nullable String origin, PageScrapedCallback delegate) {
      mOrigin = origin;
      mDelegate = delegate;
    }

    @Override
    public void onResponse(Networker.HttpResponse result) {
      ArrayList<String> imageUrls = new ArrayList<>();
      String htmlText = Utf8Charset.decodeUTF8(result.body);
      HtmlScraper.parseWithImageTags(htmlText, mOrigin, imageUrls);
      mDelegate.onPageScraped(imageUrls);
    }

    @Override
    public void onFailure(IOException e) {
      mDelegate.onPageScraped(null /* imageUrls */);
    }
  }

  private static class RssParser {
    private final XmlPullParser mParser;

    public RssParser(XmlPullParser parser) {
      mParser = parser;
    }

    public List<RssItem> parse() throws IOException, XmlPullParserException {
      ArrayList<RssItem> items = new ArrayList<RssItem>();

      mParser.nextTag();
      mParser.require(XmlPullParser.START_TAG, null, "rss");
      mParser.nextTag();
      mParser.require(XmlPullParser.START_TAG, null, "channel");

      while (mParser.next() != XmlPullParser.END_TAG) {
        if (mParser.getEventType() != XmlPullParser.START_TAG) {
          continue;
        }

        String name = mParser.getName();
        if (name.equals("item")) {
          items.add(readItem());
        } else {
          skip();
        }
      }

      return items;
    }

    private RssItem readItem() throws XmlPullParserException, IOException {
      mParser.require(XmlPullParser.START_TAG, null, "item");
      RssItem item = new RssItem();
      while (mParser.next() != XmlPullParser.END_TAG) {
        if (mParser.getEventType() != XmlPullParser.START_TAG) {
          continue;
        }
        String name = mParser.getName();
        if (name.equals("title")) {
          item.title = readTextFromTag("title");
        } else if (name.equals("description")) {
          item.description = readTextFromTag("description");
        } else if (name.equals("link")) {
          item.link = readTextFromTag("link");
        } else {
          skip();
        }
      }
      return item;
    }

    private String readTextFromTag(String tagName) throws IOException, XmlPullParserException {
      mParser.require(XmlPullParser.START_TAG, null, tagName);
      String text = readText();
      mParser.require(XmlPullParser.END_TAG, null, tagName);
      return text;
    }

    private String readText() throws IOException, XmlPullParserException {
      String result = "";
      if (mParser.next() == XmlPullParser.TEXT) {
        result = mParser.getText();
        mParser.nextTag();
      }
      return result;
    }

    private void skip() throws IOException, XmlPullParserException {
      if (mParser.getEventType() != XmlPullParser.START_TAG) {
        throw new IllegalStateException();
      }
      int depth = 1;
      while (depth != 0) {
        switch (mParser.next()) {
          case XmlPullParser.END_TAG:
            depth--;
            break;
          case XmlPullParser.START_TAG:
            depth++;
            break;
        }
      }
    }
  }

  private static class RssItem {
    public String title;
    public String description;
    public String link;
  }

  private static class ApodItem {
    public RssItem rssItem;
    @Nullable public String largeImageUrl;
  }

  private interface PageScrapedCallback {
    /**
     * @param imageUrls Image URLs that were scraped or null if the page could not be fetched or
     *     parsed.
     */
    public void onPageScraped(@Nullable List<String> imageUrls);
  }
}
